Fix ArgumentOutOfRangeException in Type and Decl constructors during ObjC AST traversal#690
Merged
tannergooding merged 1 commit intodotnet:mainfrom Mar 13, 2026
Conversation
…ObjC AST traversal
## Problem
When using ClangSharp to traverse Objective-C translation units that include
Apple system frameworks (e.g. `<Foundation/Foundation.h>`), the Type
constructor throws `ArgumentOutOfRangeException("handle")` during deep AST
traversal. This makes it impossible to bind large Objective-C frameworks like
Facebook's FBSDKCoreKit (219 headers) using tools built on ClangSharp.
The crash was discovered while using the `sharpie bind` tool (from
dotnet/macios) to generate C# bindings for iOS frameworks. The specific
crash path is:
TranslationUnitDecl.Decls
→ RecordDecl.Decls (C struct fields from Foundation)
→ FieldDecl.Type
→ PointerType.PointeeType (lazy evaluation)
→ Type.Create() dispatches to AttributedType based on TypeClass
→ AttributedType constructor passes CXType_Attributed as expectedKind
→ Type constructor: handle.kind (CXType_ObjCId) != expectedKind → THROW
## Root Cause
The `Type` constructor validates `CXTypeKind` (from libclang) BEFORE
`CX_TypeClass` (from libClangSharp), and throws when they don't match.
However, libclang's `CXTypeKind` is a coarser classification than
libClangSharp's `CX_TypeClass`. For certain Objective-C types, libclang
returns a broad kind like `CXType_ObjCId` (27) or `CXType_Unexposed` (1)
while libClangSharp correctly classifies the same type as
`CX_TypeClass_Attributed` by inspecting the Clang AST directly.
The `Type.Create()` factory method dispatches on `TypeClass` (which is
correct), but the resulting subclass constructor then rejects the handle
because `CXTypeKind` doesn't match the expected value. This is a false
rejection — `TypeClass` is the authoritative classifier and should take
precedence.
A secondary issue exists in the `Decl` constructor: the explicit check
`handle.DeclKind == CX_DeclKind_Invalid` causes an unconditional throw even
when `Decl.Create()`'s default case intentionally constructs a generic Decl
wrapper for unknown declaration kinds. The default case passes
`expectedDeclKind = handle.DeclKind = CX_DeclKind_Invalid`, which then
triggers the Invalid-specific guard. This makes the default case dead code
that always crashes instead of gracefully degrading.
## Fix
### Type.cs
- Reorder validation: check `CX_TypeClass` first (authoritative), then
`CXTypeKind` (informational)
- When `TypeClass` matches but `CXTypeKind` doesn't, accept the type
instead of throwing. This handles the common case where libclang uses a
broader kind (e.g. `CXType_ObjCId`, `CXType_Unexposed`,
`CXType_ObjCObjectPointer`) for a type that libClangSharp classifies
more precisely
### Decl.cs
- Remove the `handle.DeclKind == CX_DeclKind_Invalid` guard so that the
default case in `Decl.Create()` can construct a generic Decl wrapper for
unknown declaration kinds instead of crashing
## Tests
Added 4 new tests in `ObjectiveCTest.cs`:
- `Type_AttributedType_WithMismatchedCXTypeKind`: Parses an ObjC file with
`nullable id` parameters using Foundation headers and deeply traverses
all type information including PointeeType and AttributedType chains
- `Type_FullFoundationTraversal_DoesNotCrash`: Full recursive traversal of
all declarations from a Foundation-importing translation unit, including
ObjCContainerDecl children and RecordDecl fields (the actual crash path)
- `Type_DeepTraversal_DoesNotCrash`: Deep traversal of inline ObjC code
with interfaces, protocols, categories, methods, and properties
- `Decl_InvalidDeclKind_DoesNotCrash`: Traversal of diverse ObjC
declaration kinds including CursorChildren access
The Foundation-based tests:
- Try iPhoneOS SDK first (where the bug manifests), fall back to macOS SDK
- Use `xcrun clang --print-resource-dir` for clang resource headers
- Gracefully skip via `Assert.Ignore` if no SDK is available
- Are already guarded by `[Platform("macosx")]` on the test class
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dalexsoto
added a commit
to dotnet/macios
that referenced
this pull request
Mar 12, 2026
tannergooding
approved these changes
Mar 13, 2026
dalexsoto
added a commit
to dotnet/macios
that referenced
this pull request
Mar 14, 2026
When using `sharpie bind --header <file> --scope <dir>`, the --scope
argument was stored verbatim without path normalization. Since Clang
always reports declaration source locations as absolute paths, a
relative --scope value (e.g. `MyFramework.framework/Headers`) would
never match the absolute filename from `presumedLoc.FileName`, causing
IsInScope() to filter out every declaration and produce zero output
files — even though parsing succeeded.
Additionally, the StartsWith comparison in IsInScope() could produce
false positive matches when one directory name was a prefix of another
(e.g. scope `/tmp/scope` would incorrectly match files in
`/tmp/scopeextra/`).
~~Finally, when binding a very large number of Objective-C headers
(200+), ClangSharp can throw an ArgumentOutOfRangeException with
parameter name "handle" during AST traversal, due to cursor/type handle
misclassification in its managed wrapper layer. Sharpie caught this
exception but reported the raw, opaque message ("Specified argument was
out of the range of valid values"), giving users no guidance on how to
work around the issue.~~ -> Moved to
dotnet/ClangSharp#690
Changes:
1. Tools.cs: Normalize --scope paths to absolute via Path.GetFullPath()
when parsing CLI arguments, matching the behavior already used by
--framework mode (which calls Path.GetFullPath on SourceFramework in
ResolveFramework()).
2. ObjectiveCBinder.cs (IsInScope): Append a trailing directory
separator to scope directory paths before the StartsWith check, so that
`/tmp/scope/` does not falsely match `/tmp/scopeextra/`.
~~3. BindingResult.cs (ReportUnexpectedError): Detect the specific
ArgumentOutOfRangeException("handle") pattern from ClangSharp and report
an actionable error message that explains the root cause (translation
unit too complex) and suggests workarounds (bind fewer headers, use
--scope).~~
4. Tests: Added 5 new test cases:
- Scope_RelativePath: verifies relative --scope paths produce correct
output after normalization
- Scope_FiltersOutOfScopeDeclarations: verifies that only declarations
from in-scope headers are bound
- Scope_PrefixDoesNotFalseMatch: verifies that scope "/foo/bar" does not
match "/foo/barbaz/header.h"
- ~~HandleCrash_ReportsUsefulError: verifies the improved error message
for ClangSharp handle exceptions~~
- ~~HandleCrash_OtherExceptionsUnchanged: verifies that other exception
types still report their original message~~
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When using ClangSharp to traverse Objective-C translation units that include Apple system frameworks (e.g.
<Foundation/Foundation.h>), the Type constructor throwsArgumentOutOfRangeException("handle")during deep AST traversal. This makes it impossible to some Objective-C frameworks like Facebook's FBSDKCoreKit using tools built on ClangSharp.The crash was discovered while using the
sharpie bindtool (from dotnet/macios) to generate C# bindings for iOS frameworks. The specific crash path is:Root Cause
The
Typeconstructor validatesCXTypeKind(from libclang) BEFORECX_TypeClass(from libClangSharp), and throws when they don't match. However, libclang'sCXTypeKindis a coarser classification than libClangSharp'sCX_TypeClass. For certain Objective-C types, libclang returns a broad kind likeCXType_ObjCId(27) orCXType_Unexposed(1) while libClangSharp correctly classifies the same type asCX_TypeClass_Attributedby inspecting the Clang AST directly.The
Type.Create()factory method dispatches onTypeClass(which is correct), but the resulting subclass constructor then rejects the handle becauseCXTypeKinddoesn't match the expected value. This is a false rejection —TypeClassis the authoritative classifier and should take precedence.A secondary issue exists in the
Declconstructor: the explicit checkhandle.DeclKind == CX_DeclKind_Invalidcauses an unconditional throw even whenDecl.Create()'s default case intentionally constructs a generic Decl wrapper for unknown declaration kinds. The default case passesexpectedDeclKind = handle.DeclKind = CX_DeclKind_Invalid, which then triggers the Invalid-specific guard. This makes the default case dead code that always crashes instead of gracefully degrading.Fix
Type.cs
CX_TypeClassfirst (authoritative), thenCXTypeKind(informational)TypeClassmatches butCXTypeKinddoesn't, accept the type instead of throwing. This handles the common case where libclang uses a broader kind (e.g.CXType_ObjCId,CXType_Unexposed,CXType_ObjCObjectPointer) for a type that libClangSharp classifies more preciselyDecl.cs
handle.DeclKind == CX_DeclKind_Invalidguard so that the default case inDecl.Create()can construct a generic Decl wrapper for unknown declaration kinds instead of crashingTests
Added 4 new tests in
ObjectiveCTest.cs:Type_AttributedType_WithMismatchedCXTypeKind: Parses an ObjC file withnullable idparameters using Foundation headers and deeply traverses all type information including PointeeType and AttributedType chainsType_FullFoundationTraversal_DoesNotCrash: Full recursive traversal of all declarations from a Foundation-importing translation unit, including ObjCContainerDecl children and RecordDecl fields (the actual crash path)Type_DeepTraversal_DoesNotCrash: Deep traversal of inline ObjC code with interfaces, protocols, categories, methods, and propertiesDecl_InvalidDeclKind_DoesNotCrash: Traversal of diverse ObjC declaration kinds including CursorChildren accessThe Foundation-based tests:
xcrun clang --print-resource-dirfor clang resource headersAssert.Ignoreif no SDK is available[Platform("macosx")]on the test class